package com.craftinginterpreters.lox;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.craftinginterpreters.lox.TokenType.*;

class Scanner {

    /**
     *  用字符串的形式存储原始代码
     */
    private final String source;

    /**
     * 列表存储标记token
     */
    private final List<Token> tokens = new ArrayList<>();


    /**
     *   start指向要扫描词素的第一个字符
     */
    private int start = 0;
    /**
     *  current指向当前正在被处理的字符，在整个source的位置
     */
    private int current = 0;
    /**
     * line字段指向current所在的源代码行
     */
    private int line = 1;

    /**
     *  新增了一个map来保留关键字的集合
     */
    private static final Map<String, TokenType> keywords;

    static {
        keywords = new HashMap<>();
        keywords.put("and",    AND);
        keywords.put("class",  CLASS);
        keywords.put("else",   ELSE);
        keywords.put("false",  FALSE);
        keywords.put("for",    FOR);
        keywords.put("fun",    FUN);
        keywords.put("if",     IF);
        keywords.put("nil",    NIL);
        keywords.put("or",     OR);
        keywords.put("print",  PRINT);
        keywords.put("return", RETURN);
        keywords.put("super",  SUPER);
        keywords.put("this",   THIS);
        keywords.put("true",   TRUE);
        keywords.put("var",    VAR);
        keywords.put("while",  WHILE);
    }

    Scanner(String source) {
        this.source = source;
    }


    /**
     *
     * @return
     */
    List<Token> scanTokens() {
        while (!isAtEnd()) {
            // We are at the beginning of the next lexeme.
            start = current;
            scanToken();
        }

        // 最后添加一个EOF标记，可无，但目的为了更优雅
        tokens.add(new Token(EOF, "", null, line));
        return tokens;
    }

    /**
     *  扫描器的真正核心
     */
    private void scanToken() {
        char c = advance();
        switch (c) {
            case '(': addToken(LEFT_PAREN); break;
            case ')': addToken(RIGHT_PAREN); break;
            case '{': addToken(LEFT_BRACE); break;
            case '}': addToken(RIGHT_BRACE); break;
            case ',': addToken(COMMA); break;
            case '.': addToken(DOT); break;
            case '-': addToken(MINUS); break;
            case '+': addToken(PLUS); break;
            case ';': addToken(SEMICOLON); break;
            case '*': addToken(STAR); break;
            case '!':
                addToken(match('=') ? BANG_EQUAL : BANG);
                break;
            case '=':
                addToken(match('=') ? EQUAL_EQUAL : EQUAL);
                break;
            case '<':
                addToken(match('=') ? LESS_EQUAL : LESS);
                break;
            case '>':
                addToken(match('=') ? GREATER_EQUAL : GREATER);
                break;
            case '/':
                if (match('/')) {
                    // 处理单行双斜线注释使用
                    while (peek() != '\n' && !isAtEnd()) {
                        advance();
                    }
                } else {
                    addToken(SLASH);
                }
                break;
            case ' ':
            case '\r':
            case '\t':
                // 忽略全部空格类
                break;
            case '\n':
                line++;
                break;
            case '"':
                string();
                break;
            default:
                if (isDigit(c)) {
                    number();
                } else if (isAlpha(c)) {
                    identifier();
                }else {
                    Lox.error(line, "Unexpected character.");
                }
                break;
        }
    }

    /**
     *  标识符处理，先使劲
     */
    private void identifier() {
        while (isAlphaNumeric(peek())) {
            advance();
        }

        // 对map进行匹配
        String text = source.substring(start, current);
        TokenType type = keywords.get(text);
        if (type == null) {
            type = IDENTIFIER;
        }
        addToken(type);
    }


    private boolean isAlpha(char c) {
        return (c >= 'a' && c <= 'z') ||
                (c >= 'A' && c <= 'Z') ||
                c == '_';
    }

    private boolean isAlphaNumeric(char c) {
        return isAlpha(c) || isDigit(c);
    }

    /**
     *  处理字符串使用的函数
     */
    private void string() {
        while (peek() != '"' && !isAtEnd()) {
            if (peek() == '\n') {
                line++;
            }
            advance();
        }

        if (isAtEnd()) {
            Lox.error(line, "Unterminated string.");
            return;
        }

        // 消耗右侧的双引号
        advance();

        // 去除两边的双引号
        String value = source.substring(start + 1, current - 1);
        addToken(STRING, value);
    }

    /**
     *
     * @param c
     * @return
     */
    private boolean isDigit(char c) {
        return c >= '0' && c <= '9';
    }

    /**
     *  处理数字的方法，先消除整数部分，再处理小数点和小数部分，最后使用Double类进行parse
     */
    private void number() {
        while (isDigit(peek())) {
            advance();
        }

        // 处理小数部分
        if (peek() == '.' && isDigit(peekNext())) {
            // 先处理小数点
            advance();

            while (isDigit(peek())) {
                advance();
            }
        }

        addToken(NUMBER, Double.parseDouble(source.substring(start, current)));
    }

    /**
     *  向前看两个字符，这是peek的进阶
     * @return
     */
    private char peekNext() {
        if (current + 1 >= source.length()) {
            return '\0';
        }
        return source.charAt(current + 1);
    }


    /**
     *  advance方法会消耗源代码文件里的下一个字符，并返回给调用者。
     * @return
     */
    private char advance() {
        current++;
        return source.charAt(current - 1);
    }

    /**
     * 获取当前词素的文本，并创造出一个对应的新标记。很快我们将会用其他的
     * @param type
     */
    private void addToken(TokenType type) {
        addToken(type, null);
    }

    /**
     * 重载方法，用来处理字面值的标记。
     *  一般用来处理数字和字符串
     * @param type
     * @param literal
     */
    private void addToken(TokenType type, Object literal) {
        String text = source.substring(start, current);
        tokens.add(new Token(type, text, literal, line));
    }

    /**
     * 检测源码中所有字符是不是都消耗完了
     * @return
     */
    private boolean isAtEnd() {
        return current >= source.length();
    }


    /**
     *  应对多字符token使用，只有在确认全部字符都匹配了某个token时，才会消耗掉
     * @param expected
     * @return
     */
    private boolean match(char expected) {
        if (isAtEnd()) {
            return false;
        }
        if (source.charAt(current) != expected) {
            return false;
        }

        current++;
        return true;
    }


    /**
     *  不消耗字符向前看，用来侦查
     * @return
     */
    private char peek() {
        if (isAtEnd()) {
            return '\0';
        }
        return source.charAt(current);
    }

}